/************************************************************************
 *
 * Copyright (c) 2009
 * PrismTech Ltd.
 * All rights Reserved.
 *
 * LOGICAL_NAME:    IdentificationAppImpl.java
 * FUNCTION:        OpenSplice DDS Power Tools Tutorial example code.
 * MODULE:          Tutorial for the Java programming language.
 * DATE             April 2009.
 ************************************************************************
 *
 * This class contains the implementation of the identification application.
 *
 ***/

package Identification;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;
import java.util.TreeMap;

import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JFormattedTextField;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.text.DefaultFormatter;
import javax.swing.text.DefaultFormatterFactory;
import javax.swing.text.NumberFormatter;

import track.ArrayListComparator;
import track.ArrayListTableModel;
import track.ArrayListTableUpdater;
import track.BaseApplication;
import track.Classification_enum;
import track.ErrorHandler;
import track.PointTrack;
import track.PointTrackDataReader;
import track.PointTrackSeqHolder;
import track.TrackState;
import track.TrackStateDataReader;
import track.TrackStateDataWriter;
import track.TrackStateSeqHolder;
import DDS.ALIVE_INSTANCE_STATE;
import DDS.ANY_INSTANCE_STATE;
import DDS.ANY_SAMPLE_STATE;
import DDS.ANY_VIEW_STATE;
import DDS.HANDLE_NIL;
import DDS.LENGTH_UNLIMITED;
import DDS.NEW_VIEW_STATE;
import DDS.NOT_ALIVE_DISPOSED_INSTANCE_STATE;
import DDS.NOT_READ_SAMPLE_STATE;
import DDS.SampleInfo;
import DDS.SampleInfoSeqHolder;
import Identification.IdentificationAppWrapper.WrapperException;

public class IdentificationApp extends BaseApplication {
   // Interface elements.
   public static JFrame m_frame;
   private static JButton m_exitButton;
   private static JLabel m_labelCount;

   // The PointTrack data reader listener reference.
   private PointTrackDataReaderListenerImpl m_ptdrListener;

   // The TrackState data writer reference.
   private TrackStateDataWriter m_trackStateDataWriter;

   // The PointTrack data reader reference.
   private PointTrackDataReader m_pointTrackDataReader;

   // The time (in milliseconds) required to determine an object's
   // classification.
   private int m_classificationDelay = 2000;

   // Map of information on published TrackState instances.
   private HashMap<Integer, TrackStateInfo> m_published = new HashMap<Integer, TrackStateInfo>();

   // Map of "to be published" TrackState instances. We model the
   // identification delay
   // by placing the newly-created TrackState instances in this map, and
   // classifying/
   // publishing once the delay has been performed. At this point, the
   // instances are
   // removed from this map and placed in m_published.
   private TreeMap<Long, TrackStateInfo> m_pending = new TreeMap<Long, TrackStateInfo>(
         new LongComparator());

   // Delay (in milliseconds) in the classifier thread between checks of the
   // m_pending map.
   private int m_classifierDelay = 10;

   // TrackState message padding.
   public static int DEFAULT_PADDING = 0;
   public static String m_padding;

   // References for the table used to display the currently identified
   // objects.
   private static JTable m_table;
   private static ArrayListTableModel m_tableModel;
   private static String[] m_tableHeaders = { "ID", "Classification" };

   // Default transport priority for the TrackState data writer.
   public static int DEFAULT_TRANSPORT_PRIORITY = 6;

   // Default latency budget (for both the TrackState data writer and the
   // PointTrack
   // data reader).
   public static double DEFAULT_LATENCY_BUDGET = 0D;

   // Reference to the classifier.
   private Classifier m_classifier;

   // Reference to the thread running the classifier.
   private Thread m_classifierThread;

   /**
    * Stores the information for a published TrackState instance.
    */
   class TrackStateInfo {
      TrackState m_trackState;
      long m_trackStateHandle = 0L;
      Classification_enum m_classification;
   }

   /**
    * Listener implementation for the PointTrack data reader.
    */
   class PointTrackDataReaderListenerImpl extends
         IdentificationAppWrapper.PointTrackDataReaderListener {
      public void on_data_available(DDS.DataReader dataReader) {
         PointTrackDataReader ptDataReader = (PointTrackDataReader) dataReader;
         PointTrackSeqHolder receivedData;
         SampleInfoSeqHolder infoSeq;
         PointTrack[] tracks;
         SampleInfo[] samples;
         int status;

         synchronized (m_syncObject) {
            //
            // First take: check for disposed/unregistered instances on this
            // topic.
            //

            receivedData = new PointTrackSeqHolder();
            infoSeq = new SampleInfoSeqHolder();
            status = ptDataReader.take(receivedData, infoSeq,
                  LENGTH_UNLIMITED.value, ANY_SAMPLE_STATE.value,
                  ANY_VIEW_STATE.value,
                  NOT_ALIVE_DISPOSED_INSTANCE_STATE.value);
            ErrorHandler
                  .checkStatus(
                        status,
                        "PointTrackDataReaderListenerImpl: on_data_available: take (ANY/ANY/NOT_ALIVE_DISPOSED)");

            tracks = receivedData.value;
            samples = infoSeq.value;
            for (int i = 0; i < tracks.length; i++) {
               PointTrack track = tracks[i];
               SampleInfo sample = samples[i];
               Integer mapKey = new Integer(track.trackId);

               if (DEBUG) {
                  output("Object ID "
                        + track.trackId
                        + " has been disposed or is no longer available (instance state "
                        + sample.instance_state + ")");
               }

               // It's valid for there to be no map entry; this will happen
               // for an object that never came
               // within sensor range.
               TrackStateInfo info = (TrackStateInfo) m_published
                     .get(mapKey);
               if (info != null) {
                  // Dispose and unregister from the appropriate data
                  // writer.
                  status = m_trackStateDataWriter.dispose(
                        info.m_trackState, info.m_trackStateHandle);
                  ErrorHandler
                        .checkStatus(status,
                              "PointTrackDataReaderListenerImpl: on_data_available: dispose");
                  status = m_trackStateDataWriter.unregister_instance(
                        info.m_trackState, info.m_trackStateHandle);
                  ErrorHandler
                        .checkStatus(status,
                              "PointTrackDataReaderListenerImpl: on_data_available: unregister_instance");

                  if (DEBUG) {
                     output("Disposed/unregistered object ID "
                           + info.m_trackState.trackId);
                  }

                  // Remove from the map.
                  m_published.remove(mapKey);
               }
            }

            // Take complete.
            status = ptDataReader.return_loan(receivedData, infoSeq);
            ErrorHandler
                  .checkStatus(
                        status,
                        "PointTrackDataReaderListenerImpl: on_data_available: return_loan (ANY/ANY/NOT_ALIVE_DISPOSED)");

            //
            // Second take: retrieve new instances on this topic and provide
            // classification.
            // Unlike Sensor, we separate the take calls since
            // Identification is intended to
            // only perform classification once for each object.
            //

            receivedData = new PointTrackSeqHolder();
            infoSeq = new SampleInfoSeqHolder();
            status = ptDataReader.take(receivedData, infoSeq,
                  LENGTH_UNLIMITED.value, NOT_READ_SAMPLE_STATE.value,
                  NEW_VIEW_STATE.value, ALIVE_INSTANCE_STATE.value);
            ErrorHandler
                  .checkStatus(
                        status,
                        "PointTrackDataReaderListenerImpl: on_data_available: take (NOT_READ/NEW/ALIVE)");

            // Prepare to publish TrackState information for each new
            // PointTrack instance.
            tracks = receivedData.value;
            for (int i = 0; i < tracks.length; i++) {
               PointTrack track = tracks[i];

               Integer publishedKey = new Integer(track.trackId);
               TrackStateInfo info = (TrackStateInfo) m_published
                     .get(publishedKey);
               if (info != null) {
                  output("Object ID " + track.trackId
                        + " has already been classified as "
                        + info.m_classification.value());
                  continue;
               }

               // Create the new TrackState instance.
               TrackStateInfo newInfo = new TrackStateInfo();
               newInfo.m_trackState = new TrackState();
               newInfo.m_trackState.trackId = track.trackId;
               newInfo.m_trackState.padding = m_padding;

               // Place the instance into the pending map, with a key of
               // when the classification
               // delay will be complete.
               long pendingKeyValue = System.currentTimeMillis()
                     + (long) m_classificationDelay;
               synchronized (m_pending) {
                  // Guard against replacing an existing pending map
                  // entry.
                  Long pendingKey = new Long(pendingKeyValue);
                  while (m_pending.get(pendingKey) != null) {
                     pendingKeyValue++;
                     pendingKey = new Long(pendingKeyValue);
                  }
                  m_pending.put(pendingKey, newInfo);
               }

               if (DEBUG) {
                  output("Starting classification of object ID "
                        + newInfo.m_trackState.trackId
                        + "; will be complete after "
                        + m_classificationDelay + "ms");
               }
            }

            // Take complete.
            status = ptDataReader.return_loan(receivedData, infoSeq);
            ErrorHandler
                  .checkStatus(status,
                        "PointTrackDataReaderListenerImpl: on_data_available: return_loan");
         }
      }
   }

   /**
    * Comparator class used for the pending map.
    */
   class LongComparator implements Comparator<Long> {
      public final int compare(Long a, Long b) {
         long val1 = a.longValue();
         long val2 = b.longValue();

         if (val1 < val2)
            return -1;
         else if (val1 == val2)
            return 0;
         else
            return 1;
      }
   }

   /**
    * Classifier. Constantly checks the m_pending map, and classifies/publishes
    * entries that have completed the identification delay. At this point they
    * are moved to the m_published map.
    */
   class Classifier implements Runnable {
      boolean m_keepGoing = true;

      private void delay(long duration) {
         if (duration != 0) {
            try {
               synchronized (Classifier.this) {
                  wait(duration);
               }
            } catch (InterruptedException ie) {
               output("Delay of " + duration + "ms interrupted");
            }
         }
      }

      public String toString() {
         return "Classifier";
      }

      public void run() {
         while (m_keepGoing) {
            // Perform the required delay between map checks.
            delay(m_classifierDelay);

            // If nothing in the map, loop.
            if (m_pending.size() == 0) {
               continue;
            }

            int publishedCount = 0;
            Long pendingKey;
            synchronized (m_pending) {
               pendingKey = (Long) m_pending.firstKey();
               while (pendingKey.longValue() <= System.currentTimeMillis()) {
                  TrackStateInfo info = (TrackStateInfo) m_pending
                        .get(pendingKey);

                  // Generate the classification for this ID.
                  switch (info.m_trackState.trackId % 3) {
                  case 0:
                     info.m_trackState.classification = Classification_enum.TRACK_CLASSIFICATION_UNKNOWN;
                     break;
                  case 1:
                     info.m_trackState.classification = Classification_enum.TRACK_CLASSIFICATION_HOSTILE;
                     break;
                  case 2:
                     info.m_trackState.classification = Classification_enum.TRACK_CLASSIFICATION_FRIENDLY;
                     break;
                  }
                  ;

                  if (DEBUG) {
                     output("Object ID " + info.m_trackState.trackId
                           + " has been classed as category "
                           + info.m_trackState.classification.value());
                  }

                  // Write the information, and move from the pending and
                  // published maps.
                  publishTrackState(info);
                  m_pending.remove(pendingKey);
                  Integer publishedKey = new Integer(
                        info.m_trackState.trackId);
                  m_published.put(publishedKey, info);

                  publishedCount++;

                  // Move to the next key in the pending map, or drop out
                  // the loop if
                  // the map is now empty.
                  if (m_pending.size() == 0) {
                     break;
                  } else {
                     pendingKey = m_pending.firstKey();
                  }
               }
            }

            // Update the display table if needed.
            if (publishedCount > 0) {
               mapChanged();
            }
         }
      }
   }

   public IdentificationApp(String[] args) {
      parseArguments(args);

      // Obtain a reference to the TrackState data writer.
      m_trackStateDataWriter = IdentificationAppWrapper.ParticipantWrapper.PublisherWrapper.TrackStateDataWriterWrapper
            .getDataWriter();

      // Obtain a reference to the PointTrack data reader.
      m_pointTrackDataReader = IdentificationAppWrapper.ParticipantWrapper.SubscriberWrapper.PointTrackDataReaderWrapper
            .getDataReader();

      // Create the PointTrack listener.
      m_ptdrListener = new PointTrackDataReaderListenerImpl();

      // Start the classifier thread.
      m_classifier = new Classifier();
      m_classifierThread = new Thread(m_classifier);
      m_classifierThread.start();
   }

   private void publishTrackState(TrackStateInfo info) {
      synchronized (m_syncObject) {
         // Register the new TrackState instance.
         info.m_trackStateHandle = m_trackStateDataWriter
               .register_instance(info.m_trackState);
         if (info.m_trackStateHandle == HANDLE_NIL.value) {
            output("Error registering " + info.m_trackState.trackId);
         }
         if (DEBUG) {
            output("Registered object ID " + info.m_trackState.trackId);
         }

         int status = m_trackStateDataWriter.write(info.m_trackState,
               info.m_trackStateHandle);
         ErrorHandler.checkStatus(status, "publishTrackState: write");
      }
   }

   private synchronized void startIdentification() {
      output("Starting identification");
      int status;

      //
      // Reload the transient state, and republish the information.
      //

      TrackStateDataReader tsDataReader = IdentificationAppWrapper.ParticipantWrapper.SubscriberWrapper.TrackStateDataReaderWrapper
            .getDataReader();
      TrackStateSeqHolder receivedData = new TrackStateSeqHolder();
      SampleInfoSeqHolder infoSeq = new SampleInfoSeqHolder();

      synchronized (m_syncObject) {
         status = tsDataReader.read(receivedData, infoSeq,
               LENGTH_UNLIMITED.value, ANY_SAMPLE_STATE.value,
               ANY_VIEW_STATE.value, ANY_INSTANCE_STATE.value);
         ErrorHandler.checkStatus(status,
               "startIdentification: TrackStateDataReader read");

         TrackState[] states = receivedData.value;
         output("Loading transient state: read " + states.length
               + " item(s)");
         for (int i = 0; i < states.length; i++) {
            TrackState state = states[i];
            Integer mapKey = new Integer(state.trackId);
            TrackStateInfo info = (TrackStateInfo) m_published.get(mapKey);

            if (info == null) {
               info = new TrackStateInfo();
               info.m_trackState = state;
               info.m_classification = state.classification;
               output("Loading transient state: object ID "
                     + state.trackId
                     + " has been classified as category "
                     + state.classification.value());

               publishTrackState(info);

               m_published.put(mapKey, info);
            } else {
               output("Map was unexpectedly populated for object ID "
                     + state.trackId);
            }
         }

         status = tsDataReader.return_loan(receivedData, infoSeq);
         ErrorHandler.checkStatus(status,
               "startIdentification: TrackStateDataReader return_loan");
      }

      // Ensure the display table is updated.
      mapChanged();

      // Attach the PointTrack listener.
      try {
         IdentificationAppWrapper.ParticipantWrapper.SubscriberWrapper.PointTrackDataReaderWrapper
               .attach(m_ptdrListener);
      } catch (WrapperException e) {
         System.err.println("Cannot attach Listener: " + e.getMessage());
      }

   }

   private void stopIdentification() {
      output("Stopping identification");

      // Detach the listener.
      try {
         IdentificationAppWrapper.ParticipantWrapper.SubscriberWrapper.PointTrackDataReaderWrapper
               .detach(m_ptdrListener);
      } catch (WrapperException e) {
         System.err.println("Cannot detach Listener: " + e.getMessage());
      }
   }

   private void stopClassifier() {
      m_classifier.m_keepGoing = false;
      synchronized (m_classifier) {
         m_classifier.notify();
      }

      try {
         m_classifierThread.join();
      } catch (InterruptedException ie) {
         output("Join interrupted");
      }

      output("Classifier task stopped");
   }

   public String toString() {
      return "Identification";
   }

   /**
    * Updates the display table with the current contents of the published map.
    */
   private void mapChanged() {
      ArrayList<ArrayList<Integer>> tableData = new ArrayList<ArrayList<Integer>>();

      // Gather the data for the table from the map.
      Set<Integer> keys = m_published.keySet();
      Iterator<Integer> keysIter = keys.iterator();
      while (keysIter.hasNext()) {
         Integer key = (Integer) keysIter.next();
         TrackStateInfo info = (TrackStateInfo) m_published.get(key);

         ArrayList<Integer> tableRow = new ArrayList<Integer>();
         tableRow.add(new Integer(info.m_trackState.trackId));
         tableRow.add(new Integer(info.m_trackState.classification.value()));
         tableData.add(tableRow);
      }

      // Sort by object ID.
      ArrayListComparator comp = new ArrayListComparator();
      Collections.sort(tableData, comp);

      // Update the table.
      SwingUtilities.invokeLater(new ArrayListTableUpdater(m_tableModel,
            tableData));

      // Update the count information.
      m_labelCount.setText(Integer.toString(m_published.size()));
   }

   protected synchronized void setMessagePaddingMember(int padding,
         DataWriterControl control) {
      m_padding = createPadding(padding);
      output(control.getTitle() + ": message padding set to " + padding);
      control.messagePaddingSet();
   }

   public static void main(String[] args) {
      try {
         // Initialise the Splice application.
         IdentificationAppWrapper.start();
      } catch (WrapperException e) {
         System.err.println("Cannot start application: " + e.getMessage());
         return;
      }

      // Initialise and start the application.
      final IdentificationApp instance = new IdentificationApp(args);

      //
      // Create the display components.
      //

      // Output panel: displays the details for identified objects (including
      // transient state).
      JPanel outputPanel = new JPanel();
      outputPanel.setBorder(BorderFactory.createTitledBorder(m_bevelBorder,
            "Identified Objects"));
      outputPanel.setPreferredSize(new Dimension(600, 300));
      outputPanel.setLayout(new BorderLayout());
      m_table = new JTable();
      m_tableModel = new ArrayListTableModel(m_tableHeaders,
            new ArrayList<ArrayList<Integer>>());
      m_table.setModel(m_tableModel);
      JScrollPane sp = new JScrollPane(m_table);
      outputPanel.add(sp, BorderLayout.CENTER);

      JLabel countLabel = new JLabel("Identified Object Count: ");
      m_labelCount = new JLabel("0");
      JPanel p = new JPanel(new FlowLayout(FlowLayout.RIGHT));
      p.add(countLabel);
      p.add(m_labelCount);
      outputPanel.add(p, BorderLayout.NORTH);

      // Writer and reader controls. We don't show a control for the
      // TrackState data reader,
      // since this reader is only used to load the transient state on a
      // restart: by the time
      // the user could change the settings it has already done its work.
      DataWriterControl dwc = instance.new DataWriterControl(
            instance.m_trackStateDataWriter, "TrackState Data Writer",
            DEFAULT_LATENCY_BUDGET, DEFAULT_TRANSPORT_PRIORITY,
            DEFAULT_PADDING);
      DataReaderControl drc = instance.new DataReaderControl(
            instance.m_pointTrackDataReader, "PointTrack Data Reader",
            DEFAULT_LATENCY_BUDGET);

      // Settings panel (classification delay).
      JLabel delayLabel = new JLabel("Identification Delay (ms)");
      JFormattedTextField delayField = new JFormattedTextField(new Integer(0));
      delayField.setPreferredSize(m_controlSize);
      DefaultFormatter fmt = new NumberFormatter(new DecimalFormat(
            "#########"));
      fmt.setValueClass(delayField.getValue().getClass());
      DefaultFormatterFactory fmtFactory = new DefaultFormatterFactory(fmt,
            fmt, fmt);
      delayField.setFormatterFactory(fmtFactory);
      JPanel settingsPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
      settingsPanel.add(delayLabel);
      settingsPanel.add(delayField);
      settingsPanel.setBorder(BorderFactory.createTitledBorder(m_bevelBorder,
            "Identification Settings"));

      // Button(s).
      m_exitButton = new JButton("Exit");
      JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
      buttonPanel.add(m_exitButton);

      // Combine data reader, data writer and settings panels.
      JPanel combined = new JPanel(new BorderLayout(10, 5));
      combined.add(dwc.getPanel(), BorderLayout.NORTH);
      combined.add(drc.getPanel(), BorderLayout.CENTER);
      combined.add(settingsPanel, BorderLayout.SOUTH);

      // Combine the JVM panel with the above.
      JPanel controlPanel = new JPanel(new GridLayout(1, 2));
      controlPanel.add(instance.m_jvmControl.getPanel());
      controlPanel.add(combined);

      // Combine the button panel with the above.
      JPanel combined2 = new JPanel(new BorderLayout(10, 5));
      combined2.add(controlPanel, BorderLayout.NORTH);
      combined2.add(buttonPanel, BorderLayout.SOUTH);

      // Delay field listener.
      delayField.addPropertyChangeListener("value",
            new PropertyChangeListener() {
               public void propertyChange(PropertyChangeEvent pce) {
                  Integer val = (Integer) pce.getNewValue();
                  int delay = val.intValue();

                  instance.m_classificationDelay = delay;
                  instance.output("Set classification delay to " + delay);
               }
            });

      m_exitButton.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent ae) {
            m_exitButton.setEnabled(false);
            instance.stopIdentification();
            instance.stopClassifier();
            instance.stopJVMInformationUpdates();
            synchronized (instance) {
               instance.notify();
            }
         }
      });

      // Set up initial interface state.
      drc.setLatencyBudget(DEFAULT_LATENCY_BUDGET);
      dwc.setTransportPriority(DEFAULT_TRANSPORT_PRIORITY);
      dwc.setLatencyBudget(DEFAULT_LATENCY_BUDGET);
      dwc.setMessagePadding(DEFAULT_PADDING);
      delayField.setValue(new Integer(instance.m_classificationDelay));

      //
      // Construct the frame.
      //

      m_frame = new JFrame(instance.toString());
      m_frame.addWindowListener(new WindowAdapter() {
         public void windowClosing(WindowEvent e) {
            m_exitButton.doClick();
         }
      });

      m_frame.getContentPane().setLayout(new BorderLayout(10, 10));
      m_frame.getContentPane().add(outputPanel, BorderLayout.NORTH);
      m_frame.getContentPane().add(combined2, BorderLayout.SOUTH);
      m_frame.pack();
      m_frame.setVisible(true);

      // Start the identification.
      instance.startIdentification();

      if (runWithTimeOut) {
         instance.triggerWait(TIME_OUT);
         launchStartThread(5000L, m_exitButton);
      } else {
         // Trigger a wait on the application to keep active.
         instance.triggerWait();
      }

      try {
         // Stop the Splice application.
         IdentificationAppWrapper.stop();
      } catch (WrapperException e) {
         System.out.println("Cannot stop application: " + e.getMessage());
      }

      // Complete.
      System.out.println("IdentificationApplication.main :: exiting");
      m_frame.dispose();
   }
}
